package service

import (
	"context"
	"errors"
	"time"

	"database/sql"

	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	libsecurity "gitee.com/hp-huiw/family-expense-api/internal/security"
	"gitee.com/hp-huiw/my-go-core/config"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/errorcode"
	"gitee.com/hp-huiw/my-go-core/security"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
)

type UserActiveTokenService interface {
	QueryDevice(ctx context.Context, username string) ([]dto.UserActiveTokenDeviceResponse, error)
	IsActiveRefreshToken(ctx context.Context, username string, deviceID *string, refreshToken string) (bool, error)
	UpdateTokens(ctx context.Context, myUserDetails *security.MyUserDetails, ipAddress string, deviceInfo *dto.UserDeviceInfoRequest, systemInfo *dto.UserSystemInfoRequest) error
	UpdateEnabled(ctx context.Context, id string, username string, enabled bool) error
	DeleteByAccessToken(ctx context.Context, accessToken string) error
	DeleteByIDAndUsername(ctx context.Context, id string, username string) error
	DeleteByUserAndAccountGroupWithTx(ctx context.Context, tx *sql.Tx, username string, accountGroupID string) error
	DeleteByAccountGroupWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
}

func NewUserActiveTokenService(db *sql.DB) UserActiveTokenService {
	return &userActiveTokenService{
		db:                               db,
		userActiveTokenRepository:        repository.NewUserActiveTokenRepository(db),
		userActiveTokenManagerRepository: libsecurity.NewUserActiveTokenManagerRepository(db),
	}
}

// QueryDevice queries all devices which logged in with provided username.
func (s *userActiveTokenService) QueryDevice(ctx context.Context, username string) ([]dto.UserActiveTokenDeviceResponse, error) {
	query, err := s.userActiveTokenRepository.QueryDevice(ctx, username)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.UserActiveTokenDevice, _ int) dto.UserActiveTokenDeviceResponse {
		return dto.UserActiveTokenDeviceResponse{
			ID:          item.ID,
			DeviceID:    item.DeviceID,
			DeviceModel: item.DeviceModel,
			Enabled:     &item.Enabled,
			LastUpdated: item.LastUpdated,
		}
	})

	return result, nil
}

// IsActiveRefreshToken checks if this refresh token is still active. If the data does not exist means the token has been revoked.
// Refresh token does not expire so it will not be cached like access token.
// Because caching is for reducing database access during each api call where access token validation check is required.
func (s *userActiveTokenService) IsActiveRefreshToken(ctx context.Context, username string, deviceID *string, refreshToken string) (bool, error) {
	return s.userActiveTokenRepository.Exists(ctx, username, deviceID, refreshToken)
}

// UpdateTokens updates or inserts the access token and refresh token by username and device info.
// This can track the same user login via different agent.
// If you want to revoke the token just need to delete this row.
func (s *userActiveTokenService) UpdateTokens(ctx context.Context, myUserDetails *security.MyUserDetails, ipAddress string, deviceInfo *dto.UserDeviceInfoRequest, systemInfo *dto.UserSystemInfoRequest) error {
	addingNew := false
	activeToken, err := s.userActiveTokenRepository.QueryToken(ctx, myUserDetails.Username, deviceInfo.ID)

	if err != nil {
		return err
	}

	// If enabled == false means login from this device is forbidden.
	if activeToken != nil && !activeToken.Enabled {
		return errors.New(errorcode.ErrorUserDeviceLoginDisabled)
	}

	dateGenerated := date.GetCurrentDate()
	dateExpired := dateGenerated.Add(time.Duration(myUserDetails.ExpiresIn) * time.Second)

	if activeToken == nil {
		activeToken = &dbmodel.UserActiveToken{
			ID:          util.GenerateUUIDWithoutDashes(),
			Username:    myUserDetails.Username,
			Enabled:     true,
			DateCreated: dateGenerated,
		}

		addingNew = true
	}

	activeToken.AccountGroupID = myUserDetails.AccountGroupID
	activeToken.IPAddress = ipAddress
	activeToken.DeviceID = deviceInfo.ID
	activeToken.DeviceBrand = deviceInfo.Brand
	activeToken.DeviceModel = deviceInfo.Model
	activeToken.DeviceProductName = deviceInfo.ProductName
	activeToken.DeviceSystemName = deviceInfo.SystemName
	activeToken.DeviceSystemVersion = deviceInfo.SystemVersion
	activeToken.OperatingSystem = systemInfo.OperatingSystem
	activeToken.OperatingSystemVersion = systemInfo.OperatingSystemVersion
	activeToken.PlatformVersion = systemInfo.PlatformVersion
	activeToken.AppVersion = systemInfo.AppVersion
	activeToken.Expiration = int32(config.ConfigLoader.GetAppConfig().Jwt.Expiration)
	activeToken.AccessToken = myUserDetails.AccessToken
	activeToken.RefreshToken = myUserDetails.RefreshToken
	activeToken.DateGenerated = dateGenerated
	activeToken.DateExpired = dateExpired
	activeToken.LastUpdated = dateGenerated

	if addingNew {
		if err = s.userActiveTokenRepository.Insert(ctx, activeToken); err != nil {
			return err
		}
	} else {
		if err = s.userActiveTokenRepository.Update(ctx, activeToken); err != nil {
			return err
		}
	}

	s.userActiveTokenManagerRepository.ReloadCachedUserActiveTokens(ctx)

	return nil
}

// UpdateEnabled updates the enabled property for the user active token with provided id.
func (s *userActiveTokenService) UpdateEnabled(ctx context.Context, id string, username string, enabled bool) error {
	err := s.userActiveTokenRepository.UpdateEnabled(ctx, id, username, enabled)

	if err != nil {
		return err
	}

	s.userActiveTokenManagerRepository.ReloadCachedUserActiveTokens(ctx)

	return nil
}

// DeleteByAccessToken deletes active data by access token.
func (s *userActiveTokenService) DeleteByAccessToken(ctx context.Context, accessToken string) error {
	err := s.userActiveTokenRepository.DeleteByAccessToken(ctx, accessToken)

	if err != nil {
		return err
	}

	s.userActiveTokenManagerRepository.ReloadCachedUserActiveTokens(ctx)

	return nil
}

// DeleteByIDAndUsername deletes active data by id and username.
func (s *userActiveTokenService) DeleteByIDAndUsername(ctx context.Context, id string, username string) error {
	err := s.userActiveTokenRepository.DeleteByIDAndUsername(ctx, id, username)

	if err != nil {
		return err
	}

	s.userActiveTokenManagerRepository.ReloadCachedUserActiveTokens(ctx)

	return nil
}

// DeleteByUserAndAccountGroupWithTx deletes active data by username with transaction.
func (s *userActiveTokenService) DeleteByUserAndAccountGroupWithTx(ctx context.Context, tx *sql.Tx, username string, accountGroupID string) error {
	err := s.userActiveTokenRepository.DeleteByUserAndAccountGroupWithTx(ctx, tx, username, accountGroupID)

	if err != nil {
		return err
	}

	s.userActiveTokenManagerRepository.ReloadCachedUserActiveTokens(ctx)

	return nil
}

// DeleteByAccountGroupWithTx deletes active data by username with transaction.
func (s *userActiveTokenService) DeleteByAccountGroupWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	err := s.userActiveTokenRepository.DeleteByAccountGroupWithTx(ctx, tx, accountGroupID)

	if err != nil {
		return err
	}

	s.userActiveTokenManagerRepository.ReloadCachedUserActiveTokens(ctx)

	return nil
}

// Container of UserActiveTokenService functions.
type userActiveTokenService struct {
	db                               *sql.DB
	userActiveTokenRepository        repository.UserActiveTokenRepository
	userActiveTokenManagerRepository *libsecurity.UserActiveTokenManagerRepository
}
